// ==UserScript==
// @name        OH repo utils 
// @namespace   Violentmonkey Scripts
// @grant       none
// @version     1.0.9
// @author      milkpotatoes
// @homepageURL https://gitee.com/milkpotatoes/tools-for-openharmony-repo
// @description Utils for openharmony repo tools
// ==/UserScript==

'use strict';

class PrUtils {
    static isLogin() {
        throw new Error('PrUtils not has function.');
    }
    static getPRAuthor() {
        throw new Error('PrUtils not has function.');
    }
    static getLoginUser() {
        throw new Error('PrUtils not has function.');
    }
    static getWebLang() {
        return navigator.language;
    }
    static getPRState() {
        throw new Error('PrUtils not has function.');
    }
    static getPRPage() {
        throw new Error('PrUtils not has function.');
    }

    static getRepoInfo() {
        const path = location.pathname;

        const [, author, repo, page, id] = path.split('/');
        return {
            author: author,
            repo: repo,
            page: page,
            id: parseInt(id),
        }
    }
}

const g_repoMatcherRegExpCache = new Map();
const g_historyEventTarget = new EventTarget();
const ORIGIN_HISTORY_METHODS = {
    back: undefined,
    forward: undefined,
    go: undefined,
    pushState: undefined,
    replaceState: undefined,
    hooked: false,
}

class Utils {
    static prUtils = PrUtils;
    static bindPrUtils(utils) {
        Utils.prUtils = utils;
    }
    /** 
     * State of pull request
     * 
     * @readonly
     * @enum {number}
     */
    static PR_STATE = this.genEnumClass({
        UNKNOWN: 0,
        CLOSED: 1,
        OPEN: 2,
        MERGED: 3,
        CONFLICTED: 4,
        DRAFT: 8,
    });

    /** 
     * Page of pull request
     * 
     * @readonly
     * @enum {number}
     */
    static PR_PAGE = this.genEnumClass({
        UNKNOWN: 0,
        COMMENTS: 1,
        COMMITS: 2,
        FILES: 3,
        CHECKS: 4,
    });

    static genEnumClass(obj) {
        const enumObj = {};
        let lastIndex = 0;
        let valType = 'number';
        for (let k in obj) {
            const v = obj[k];
            if (typeof k !== 'string') {
                throw TypeError('invalid enum object, key must be string');
            }
            if (typeof v !== 'string' && typeof v !== 'number' && v !== undefined) {
                throw TypeError('invalid enum object, value must be string of number');
            }
            if (lastIndex === 0) {
                valType = typeof v;
            }
            if (valType === 'string') {
                if (v === undefined) {
                    throw TypeError('invalid enum object, value of string enum cannot be empty.')
                }
                if (typeof v !== 'string') {
                    throw TypeError('invalid enum object, value of string enum must be string.')
                }
            } else {
                if (typeof v !== 'number') {
                    throw TypeError('invalid enum object, value of number enum must be number or empty.')
                }
                lastIndex = Math.max(lastIndex, v);
            }
            let val = lastIndex;
            if (v !== undefined) {
                val = v;
            }
            enumObj[k] = val;
            enumObj[String(val)] = k;

            lastIndex++;
        }

        Object.freeze(enumObj);
        return enumObj;
    }

    static getI18NRes(resources, prefer) {
        if (typeof resources === 'string') {
            return resources;
        }

        const processLink = (res) => {
            if (res.startsWith('@')) {
                const linkRes = resources[res.substring(1)];
                if (linkRes === undefined) {
                    throw Error(`invalid resource link, resource "${res}" not found.`);
                }
                return processLink(linkRes);
            }
            return res;
        }

        const lang = prefer ?? this.prUtils.getWebLang();
        let res = resources[lang];
        if (res !== undefined) {
            return processLink(res);
        }

        const noLocale = lang.split('-')[0];
        res = resources[noLocale];
        if (res === undefined) {
            for (const k in resources) {
                if (k !== 'default' && k.startsWith(noLocale)) {
                    return resources[k];
                }
            }
        }
        if (res !== undefined) {
            return res;
        }

        const defaultKey = resources['default'];
        if (defaultKey === undefined) {
            throw Error('invalid resource, no default resource found.');
        }
        return processLink(defaultKey);
    }

    /**
     * Check repo is statisfied condification
     * 
     * @param {string} cond condification
     * @param {Object} info info of repo
     */
    static matchRepo(cond, info) {
        const reg = g_repoMatcherRegExpCache.get(cond) ?? new RegExp(cond.replace(/\*/g, '(.+?)'));
        g_repoMatcherRegExpCache.set(cond, reg);
        return info.repo.match(reg) !== null;
    }

    static getButtonEnabled(btnInfo) {
        const enabled = { repo: false, state: false, page: false };
        const { enable, disable } = btnInfo;

        const prefix = { page: 'page:', state: 'state:' };
        const info = { repo: this.prUtils.getRepoInfo(), page: this.prUtils.getPRPage(), state: this.prUtils.getPRState() }

        if (enable) {
            const matched = { repo: false, state: false, page: false }
            enable.forEach((cond) => {
                if (cond.startsWith(prefix.state)) {
                    matched.state = true;
                    enabled.state = enabled.state || info.state === this.PR_STATE[cond.substring(prefix.state.length)];
                } else if (cond.startsWith(prefix.page)) {
                    matched.page = true;
                    enabled.page = enabled.page || info.page === this.PR_PAGE[cond.substring(prefix.page.length)];
                } else {
                    matched.repo = true;
                    enabled.repo = enabled.repo || this.matchRepo(cond, info.repo);
                }
            });

            if (!matched.repo) {
                enabled.repo = true;
            };
            if (!matched.state) {
                enabled.state = true;
            };
            if (!matched.page) {
                enabled.page = true;
            };
        } else {
            [enabled.repo, enabled.state, enabled.page] = [true, true, true];
        }

        if (disable) {
            disable.forEach((cond) => {
                if (cond.startsWith(prefix.state)) {
                    enabled.state = enabled.state && info.state !== this.PR_STATE[cond.substring(prefix.state.length)];
                } else if (cond.startsWith(prefix.page)) {
                    enabled.page = enabled.page && info.page !== this.PR_PAGE[cond.substring(prefix.page.length)];
                } else {
                    enabled.repo = enabled.repo && !this.matchRepo(cond, info.repo);
                }
            });
        }

        return enabled.repo && enabled.state && enabled.page;
    }

    static getHistoryEventTarget() {
        return g_historyEventTarget;
    }

    static hookHistoryMethod() {
        if (ORIGIN_HISTORY_METHODS.hooked) {
            return;
        }
        ORIGIN_HISTORY_METHODS.hooked = true;
        ORIGIN_HISTORY_METHODS.back = history.back.bind(history);
        ORIGIN_HISTORY_METHODS.forward = history.forward.bind(history);
        ORIGIN_HISTORY_METHODS.go = history.go.bind(history);
        ORIGIN_HISTORY_METHODS.pushState = history.pushState.bind(history);
        ORIGIN_HISTORY_METHODS.replaceState = history.replaceState.bind(history);

        history.back = function back() {
            ORIGIN_HISTORY_METHODS.back();
            g_historyEventTarget.dispatchEvent(new Event('back'));
        }
        history.forward = function forward() {
            ORIGIN_HISTORY_METHODS.forward();
            g_historyEventTarget.dispatchEvent(new Event('forward'));
        }
        history.go = function go(...args) {
            const ev = new Event('go');
            Reflect.set(ev, 'data', args);
            ORIGIN_HISTORY_METHODS.go(...args);
            g_historyEventTarget.dispatchEvent(ev);
        }
        history.pushState = function pushState(...args) {
            const ev = new Event('pushstate');
            Reflect.set(ev, 'data', args);
            ORIGIN_HISTORY_METHODS.pushState(...args);
            g_historyEventTarget.dispatchEvent(ev);
        }
        history.replaceState = function replaceState(...args) {
            const ev = new Event('replacestate');
            Reflect.set(ev, 'data', args);
            ORIGIN_HISTORY_METHODS.replaceState(...args);
            g_historyEventTarget.dispatchEvent(ev);
        }
    }

    /**
     * 
     * @param {string} str 
     * @returns {string}
     */
    static escapeHtml(str) {
        return str.replace(/[&<>"']/g, (m) => ({
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#39;'
        }[m]));
    }
}

class StyleLists {
    add(style) {
        this.list.push(style);
    }

    remove(style, count) {
        if (typeof style === 'number') {
            this.list.splice(style, count);
        } else {
            for (let i = 0; i < this.list.length; i++) {
                if (this.list[i] === style) {
                    this.list.splice(i, 1);
                }
            }
        }
    }

    at(index) {
        return this.list[index];
    }

    size() {
        return this.list.length;
    }

    apply(element) {
        this.list.forEach((css) => {
            element.adoptedStyleSheets.push(css);
        });
    }

    forEach(callback) {
        if (callback === undefined || callback === null) {
            return;
        }
        if (typeof callback !== 'function') {
            throw TypeError('invalid arg, callback must be function');
        }
        const cb = callback.bind(this);
        for (let i = 0; i < this.list.length; i++) {
            cb(this.list[i], i, this);
        }
    };

    list = [];
}

class MaterialIcon {
    static ICON_TYPE = Utils.genEnumClass({
        DEFAULT: - 1,
        FILLED: 1,
        OUTLINED: 1 << 1,
        ROUND: 1 << 2,
        SHARP: 1 << 3,
        TWO_TONE: 1 << 4,
        ALL: (1 << 5) - 1,
    });

    static ICON_CSS = Utils.genEnumClass({
        ALL: 'MD_ICON_ALL_CSS',
        FILLED: 'MD_ICON_FILLED_CSS',
        OUTLINED: 'MD_ICON_OUTLINED_CSS',
        ROUND: 'MD_ICON_ROUND_CSS',
        SHARP: 'MD_ICON_SHARP_CSS',
        TWO_TONE: 'MD_ICON_TWO_TONE_CSS',
    });

    static ICON_WOFF = Utils.genEnumClass({
        FILLED: 'MD_ICON_FILLED_WOFF',
        OUTLINED: 'MD_ICON_OUTLINED_WOFF',
        ROUND: 'MD_ICON_ROUND_WOFF',
        SHARP: 'MD_ICON_SHARP_WOFF',
        TWO_TONE: 'MD_ICON_TWO_TONE_WOFF',
    });

    static ICON_WOFF2 = Utils.genEnumClass({
        FILLED: 'MD_ICON_FILLED_WOFF2',
        OUTLINED: 'MD_ICON_OUTLINED_WOFF2',
        ROUND: 'MD_ICON_ROUND_WOFF2',
        SHARP: 'MD_ICON_SHARP_WOFF2',
        TWO_TONE: 'MD_ICON_TWO_TONE_WOFF2',
    });

    static FONTS = this.initFontFace({
        FILLED: { name: 'Material Icons', font: null, },
        OUTLINED: { name: 'Material Icons Outlined', font: null, },
        ROUND: { name: 'Material Icons Round', font: null, },
        SHARP: { name: 'Material Icons Sharp', font: null, },
        TWO_TONE: { name: 'Material Icons Two Tone', font: null, },
    });

    static initFontFace(fonts) {
        for (let name in fonts) {
            const res = fonts[name];
            GM.getResourceUrl(this.ICON_WOFF2[name])
                .then(async (woff2) => {
                    const woff = await GM.getResourceUrl(this.ICON_WOFF2[name]);
                    res.font = new FontFace(res.name,
                        `url("${woff}") format("woff2"), ` +
                        `url("${woff2}") format("woff")`);
                });
        }
        return fonts;
    }

    static cachedStyles = new Map();

    static async processRealFonts(style) {
        switch (style) {
            case this.ICON_CSS.ALL:
                await this.FONTS.FILLED.font.load();
                await this.FONTS.OUTLINED.font.load();
                await this.FONTS.ROUND.font.load();
                await this.FONTS.SHARP.font.load();
                await this.FONTS.TWO_TONE.font.load();

                document.fonts.add(this.FONTS.FILLED.font);
                document.fonts.add(this.FONTS.OUTLINED.font);
                document.fonts.add(this.FONTS.ROUND.font);
                document.fonts.add(this.FONTS.SHARP.font);
                document.fonts.add(this.FONTS.TWO_TONE.font);
                break;
            case this.ICON_CSS.FILLED:
                await this.FONTS.FILLED.font.load();
                document.fonts.add(this.FONTS.FILLED.font);
                break;
            case this.ICON_CSS.OUTLINED:
                await this.FONTS.OUTLINED.font.load();
                document.fonts.add(this.FONTS.OUTLINED.font);
                break;
            case this.ICON_CSS.ROUND:
                await this.FONTS.ROUND.font.load();
                document.fonts.add(this.FONTS.ROUND.font);
                break;
            case this.ICON_CSS.SHARP:
                await this.FONTS.SHARP.font.load();
                document.fonts.add(this.FONTS.SHARP.font);
                break;
            case this.ICON_CSS.TWO_TONE:
                await this.FONTS.TWO_TONE.font.load();
                document.fonts.add(this.FONTS.TWO_TONE.font);
                break;
            default:
                return;
        }
        return await GM.getResourceText(style);
    }

    static async genMDIconStyle(css) {
        if (this.cachedStyles.has(css)) {
            return this.cachedStyles.get(css);
        }
        const sheet = new CSSStyleSheet();
        await sheet.replace(await this.processRealFonts(css));
        this.cachedStyles.set(css, sheet);
        return sheet;
    }

    static async getMDIconCss(mode) {
        const styles = new StyleLists();
        let md = mode;
        if (mode == undefined || mode === this.ICON_TYPE.DEFAULT) {
            md = this.ICON_TYPE.ALL;
        }
        if (md & this.ICON_TYPE.ALL > 0) {
            styles.add(await this.genMDIconStyle(this.ICON_CSS.ALL));
            return styles;
        }
        if (md & this.ICON_TYPE.FILLED > 0) {
            styles.add(await this.genMDIconStyle(this.ICON_CSS.FILLED));
        }
        if (md & this.ICON_TYPE.OUTLINED > 0) {
            styles.add(await this.genMDIconStyle(this.ICON_CSS.OUTLINED));
        }
        if (md & this.ICON_TYPE.ROUND > 0) {
            styles.add(await this.genMDIconStyle(this.ICON_CSS.ROUND));
        }
        if (md & this.ICON_TYPE.SHARP > 0) {
            styles.add(await this.genMDIconStyle(this.ICON_CSS.SHARP));
        }
        if (md & this.ICON_TYPE.TWO_TONE > 0) {
            styles.add(await this.genMDIconStyle(this.ICON_CSS.TWO_TONE));
        }
        return styles;
    }
}
